/*
 * xpet:
 *
 * a suckless, X (and hopefully wayland) implementation of
 * xneko and other on screen pets
 *
 * > uint 2025
*/

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <time.h>

#include <X11/Xlib.h>
#include <X11/extensions/shape.h>
#include <X11/xpm.h>

#include "xpet.h"
#include "config.h"

void create_window(void);
void draw_bubble(void);
void get_mouse_pos(void);
void grab_keys(void);
void hide_speech_bubble(void);
void load_animations(void);
void move_to(int tx, int ty);
enum state find_octant(int dx, int dy);
void on_button_press(XButtonEvent* b);
void on_button_release(XButtonEvent* b);
void on_key(KeySym sym);
void on_motion(XMotionEvent* m);
void pick_random_destination(void);
void quit(void);
void run(void);
void set_pet_state(enum state ns);
void setup(void);
void show_speech_bubble(const char* s);
void step(enum state d, double* step_x, double* step_y);
void update_animation(void);
void wander(void);
Bool walking(enum state s);
void xsleep(long ms);

Display* dpy;
Window root;
int scr;
int scr_width;
int scr_height;
XpmAttributes xpm_attrs;
XFontSet font_set;

struct mouse mouse;
struct pet pet;

void create_window(void)
{
	pet.x = scr_width / 2;
	pet.y = scr_height / 2;
	pet.subpixel_x = pet.x;
	pet.subpixel_y = pet.y;

	XSetWindowAttributes attrs;
	attrs.override_redirect = True;
	pet.window = XCreateWindow(
		dpy, root, pet.x, pet.y, 64, 64, 0, CopyFromParent,
		InputOutput, CopyFromParent, CWOverrideRedirect, &attrs
	);
	struct frame* f = &pet.current_animation->frames[0];
	XShapeCombineMask(
		dpy, pet.window, ShapeBounding,
		0, 0, f->mask, ShapeSet
	);
	XSetWindowBackgroundPixmap(dpy, pet.window, f->pix);
	XSelectInput(
		dpy, pet.window,
		ButtonPressMask | ButtonReleaseMask | PointerMotionMask
	);
	XMapWindow(dpy, pet.window);
	XClearWindow(dpy, pet.window);
	XRaiseWindow(dpy, pet.window);
}

void draw_bubble(void)
{
	if (!pet.speech || !pet.bubble_window) {
		return;
	}

	XClearWindow(dpy, pet.bubble_window);
	GC graphics_context = DefaultGC(dpy, scr);
	XSetForeground(dpy, graphics_context, BlackPixel(dpy, scr));

	if (font_set) {
		XRectangle ink;
		XRectangle log;

		XmbTextExtents(font_set, pet.speech, (int)strlen(pet.speech), &ink, &log);
		XmbDrawString(
			dpy, pet.bubble_window, font_set, graphics_context,
			SPEECH_PAD_X, SPEECH_PAD_Y - log.y, pet.speech, strlen(pet.speech)
		);
	}
	else {
		XFontStruct* f = XQueryFont(dpy, XGContextFromGC(graphics_context));
		int ascent = f ? f->ascent : 12;
		XDrawString(
			dpy, pet.bubble_window, graphics_context,
			SPEECH_PAD_X, SPEECH_PAD_Y + ascent, pet.speech, strlen(pet.speech)
		);

		if (f) {
			XFreeFontInfo(NULL, f, 1);
		}
	}
}

void get_mouse_pos(void)
{
	Window root_ret;
	Window child_ret;
	int x_ret;
	int y_ret;
	int winx_ret ;
	int winy_ret ;
	unsigned int mask_ret;

	XQueryPointer(
		dpy, root, &root_ret, &child_ret,
		&x_ret, &y_ret, &winx_ret, &winy_ret, &mask_ret
	);

	mouse.x = x_ret;
	mouse.y = y_ret;
}

void grab_keys(void)
{
	unsigned int mods[] = {
		0, LockMask, Mod2Mask, LockMask | Mod2Mask
	};

	for (unsigned i = 0; i < N_KEYBINDS; i++) {
		KeyCode code = XKeysymToKeycode(dpy, bindings[i].sym);
		for (unsigned j = 0; j < sizeof(mods) / sizeof(mods[0]); j++) {
			XGrabKey(
				dpy, code, bindings[i].mask | mods[j], root, True,
				GrabModeAsync, GrabModeAsync
			);
		}
	}
}

void hide_speech_bubble(void)
{
	if (pet.bubble_window) {
		XUnmapWindow(dpy, pet.bubble_window);
	}

	pet.speech = NULL;
	pet.speech_time = 0;
}

void load_animations(void)
{
	memset(&xpm_attrs, 0, sizeof(xpm_attrs));
	for (int i = 0; i < STATE_LAST; i++) {
		int frame_count = animations[i].n_frames;
		if (frame_count == 0) {
			char path[512];
			frame_count = 0;

			for (;;) {
				snprintf(path, sizeof(path), "%s/%s/%d.xpm",
					PET_ASSET_DIR, animations[i].name, frame_count
				);

				FILE* f = fopen(path, "r");
				if (!f) {
					break;
				}

				fclose(f);
				frame_count++;
			}

			if (frame_count == 0) {
				fprintf(stderr, "xpet: warning: no frames for '%s'\n",
					animations[i].name
				);
				animations[i].frames = NULL;
				continue;
			}
		}

		animations[i].frames = malloc(sizeof(struct frame) * frame_count);
		if (!animations[i].frames) {
			perror("malloc");
			exit(1);
		}

		for (int frame_index = 0; frame_index < frame_count; frame_index++) {
			char path[512];
			snprintf(path, sizeof(path), "%s/%s/%d.xpm",
				PET_ASSET_DIR, animations[i].name, frame_index
			);

			Pixmap pixmap = 0;
			Pixmap mask = 0;
			int xpm_result = XpmReadFileToPixmap(
				dpy, root, path, &pixmap, &mask, &xpm_attrs
			);

			if (xpm_result != XpmSuccess) {
				fprintf(stderr, "xpet: load failed '%s': %d\n", path, xpm_result);
				exit(EXIT_FAILURE);
			}
			animations[i].frames[frame_index].pix = pixmap;
			animations[i].frames[frame_index].mask = mask;
			animations[i].frames[frame_index].duration =
				animations[i].frame_durations ?
				animations[i].frame_durations[frame_index] : FRAME_DURATION;
		}
		animations[i].n_frames = frame_count;
	}
}

void move_to(int target_x, int target_y)
{
	double delta_x = target_x - pet.subpixel_x;
	double delta_y = target_y - pet.subpixel_y;
	double distance_squared = delta_x * delta_x + delta_y * delta_y;

	if (distance_squared < 4.0) {
		set_pet_state(IDLE);
		return;
	}

	enum state direction = find_octant((int)delta_x, (int)delta_y);
	double step_x = 0;
	double step_y = 0;
	step(direction, &step_x, &step_y);

	if (distance_squared <= (PET_SPEED * PET_SPEED)) {
		pet.subpixel_x = target_x;
		pet.subpixel_y = target_y;
	}
	else {
		pet.subpixel_x += step_x;
		pet.subpixel_y += step_y;
	}

	pet.x = (int)(pet.subpixel_x + (pet.subpixel_x >= 0 ? 0.5 : -0.5));
	pet.y = (int)(pet.subpixel_y + (pet.subpixel_y >= 0 ? 0.5 : -0.5));
	set_pet_state(direction);

	XMoveWindow(dpy, pet.window, pet.x, pet.y);
}

enum state find_octant(int delta_x, int delta_y)
{
	int abs_delta_x = ABS(delta_x);
	int abs_delta_y = ABS(delta_y);

	/* if horizontal movement stronger than vertical */
	if (abs_delta_x > abs_delta_y * 2) {
		return delta_x > 0 ? E : W;
	}

	/* if vertical movement stronger than horizontal */
	if (abs_delta_y > abs_delta_x * 2) {
		return delta_y > 0 ? S : N;
	}

	return delta_x > 0 ?
		(delta_y < 0 ? NE : SE) : (delta_y < 0 ? NW : SW); 
}

void on_button_press(XButtonEvent* b)
{
	if (b->button == Button1) {
		pet.was_chasing = pet.chasing;
		pet.was_frozen = pet.frozen;
		pet.dragging = True;
		pet.drag_offset_x = b->x;
		pet.drag_offset_y = b->y;
		pet.frozen = True;
		pet.chasing = False;
		set_pet_state(DRAGGED);
	}
	else if (b->button == Button3) {
		if (pet.state != HAPPY) {
			pet.previous_state = pet.state;
			set_pet_state(HAPPY);
			pet.happy_time = 0;

			int n = 0;
			while (pet_phrases[n]) {
				n++;
			}

			if (n > 0) {
				show_speech_bubble(pet_phrases[rand() % n]);
			}
		}
	}
}

void on_button_release(XButtonEvent* b)
{
	if (b->button != Button1) {
		return;
	}

	pet.dragging = False;
	pet.frozen = pet.was_frozen;
	pet.chasing = pet.was_chasing;
	pet.subpixel_x = pet.x;
	pet.subpixel_y = pet.y;

	if (pet.was_frozen) {
		set_pet_state(IDLE);
		pet.frozen_time = 0;
	}
	else if (!pet.was_chasing) {
		pick_random_destination();
		set_pet_state(IDLE);
	}
	else {
		set_pet_state(E);
	}
}

void on_key(KeySym sym)
{
	if (sym == bindings[0].sym) { /* chasing */
		pet.chasing = !pet.chasing;
		if (pet.chasing) {
			pet.frozen = False;
			set_pet_state(E);
		}
		else {
			pick_random_destination();
		}
	}
	else if (sym == bindings[1].sym) { /* freeze */
		pet.frozen = !pet.frozen;
		if (pet.frozen) {
			set_pet_state(IDLE);
			pet.frozen_time = 0;
		}
	}
	else if (sym == bindings[2].sym) { /* quit */
		quit();
	}
}

void on_motion(XMotionEvent* m)
{
	(void)m;
	if (!pet.dragging) {
		return;
	}

	get_mouse_pos();
	pet.x = mouse.x - pet.drag_offset_x;
	pet.y = mouse.y - pet.drag_offset_y;

	XMoveWindow(dpy, pet.window, pet.x, pet.y);
	if (pet.speech) {
		show_speech_bubble(pet.speech);
	}
}

void pick_random_destination(void)
{
	int min_x = WANDER_MARGIN;
	int min_y = WANDER_MARGIN;
	int max_x = scr_width - WANDER_MARGIN - 64;
	int max_y = scr_height - WANDER_MARGIN - 64;

	/* clamping */
	if (max_x < min_x) {
		max_x = min_x;
	}

	if (max_y < min_y) {
		max_y = min_y;
	}

	pet.target_x = min_x + (rand() % (max_x - min_x + 1));
	pet.target_y = min_y + (rand() % (max_y - min_y + 1));
	pet.wander_wait = 0;
}

void quit(void)
{
	if (pet.bubble_window) {
		XDestroyWindow(dpy, pet.bubble_window);
	}
	if (font_set) {
		XFreeFontSet(dpy, font_set);
	}
	for (int i = 0; i < STATE_LAST; i++) {
		if (animations[i].frames) {
			for (int j = 0; j < animations[i].n_frames; j++) {
				XFreePixmap(dpy, animations[i].frames[j].pix);
				XFreePixmap(dpy, animations[i].frames[j].mask);
			}
			free(animations[i].frames);
		}
	}
	XCloseDisplay(dpy);
	exit(EXIT_SUCCESS);
}

void run(void)
{
	for (;;) {
		while (XPending(dpy)) {
			XEvent ev;
			XNextEvent(dpy, &ev);
			if (ev.type == KeyPress) {
				on_key(XLookupKeysym(&ev.xkey, 0));
			}
			else if (ev.type == ButtonPress && ev.xbutton.window == pet.window) {
				on_button_press(&ev.xbutton);
			}
			else if (ev.type == ButtonRelease && ev.xbutton.window == pet.window) {
				on_button_release(&ev.xbutton);
			}
			else if (ev.type == MotionNotify && ev.xmotion.window == pet.window) {
				on_motion(&ev.xmotion);
			}
			else if (ev.type == Expose && ev.xexpose.window == pet.bubble_window) {
				draw_bubble();
			}
		}
		if (pet.speech) {
			pet.speech_time += PET_REFRESH;
			if (pet.speech_time >= SPEECH_DURATION) {
				hide_speech_bubble();
			}
		}
		if (pet.state == HAPPY) {
			pet.happy_time += PET_REFRESH;
			if (pet.happy_time >= HAPPY_DURATION) {
				set_pet_state(pet.previous_state);
			}
		}
		else if (pet.dragging) { /* animate only */
		}
		else if (!pet.frozen) {
			if (pet.chasing) {
				get_mouse_pos();
				move_to(mouse.x, mouse.y);
			}
			else {
				wander();
			}
		}
		else {
			pet.frozen_time += PET_REFRESH;
			if (pet.frozen_time >= SLEEP_DELAY && pet.state != SLEEPING) {
				set_pet_state(SLEEPING);
			}
		}

		update_animation();

		/* keep windows on top of others */
		XRaiseWindow(dpy, pet.window);
		if (pet.bubble_window) {
			XRaiseWindow(dpy, pet.bubble_window);
		}

		xsleep(PET_REFRESH);
	}
}

void set_pet_state(enum state new_state)
{
	if (pet.state != new_state) {
		if (walking(pet.state) && walking(new_state)) {
			int current_frame = pet.current_frame;
			long frame_time = pet.frame_time;
			pet.state = new_state;
			pet.current_animation = &animations[new_state];

			if (pet.current_animation->n_frames > 0) {
				pet.current_frame = current_frame % pet.current_animation->n_frames;
				int dur = pet.current_animation->frames[pet.current_frame].duration;
				pet.frame_time = (frame_time < dur) ? frame_time : 0;
			}
			else {
				pet.current_frame = pet.frame_time = 0;
			}
		}
		else {
			pet.state = new_state;
			pet.current_animation = &animations[new_state];
			pet.current_frame = 0;
			pet.frame_time = 0;
		}
	}
	else if (pet.current_animation != &animations[new_state]) {
		pet.current_animation = &animations[new_state];
		if (!walking(new_state)) {
			pet.current_frame = pet.frame_time = 0;
		}
		else if (pet.current_animation->n_frames > 0) {
			pet.current_frame %= pet.current_animation->n_frames;
			int duration = pet.current_animation->frames[pet.current_frame].duration;

			if (pet.frame_time >= duration) {
				pet.frame_time = 0;
			}
		}
	}
}

void setup(void)
{
	/* for utf8 support */
	setlocale(LC_ALL, "");

	dpy = XOpenDisplay(NULL);
	if (!dpy) {
		fputs("xpet: no display\n", stderr);
		exit(1);
	}

	scr = DefaultScreen(dpy);
	scr_width = DisplayWidth(dpy, scr);
	scr_height = DisplayHeight(dpy, scr);
	root = RootWindow(dpy, scr);

	char** missing; /* missing character sets */
	int n_missing;
	char* def; /* default string */
	const char* patterns[] = {
		"-*-*-medium-r-*-*-14-*-*-*-*-*-*-*",
		"fixed",
		"*",
		NULL
	};

	for (int i = 0; !font_set && patterns[i]; i++) {
		font_set = XCreateFontSet(
			dpy, patterns[i], &missing, &n_missing, &def
		);

		if (font_set && n_missing > 0) {
			XFreeStringList(missing);
		}
	}

	if (!font_set) {
		fputs("xpet: warn: no fontset; non-ascii will fallback\n", stderr);
	}

	srand((unsigned)time(NULL));
	load_animations();

	pet.current_frame = 0;
	pet.frame_time = 0;
	pet.frozen_time = 0;
	pet.happy_time = 0;
	pet.previous_state = IDLE;
	pet.dragging = False;
	pet.was_chasing = False;
	pet.was_frozen = False;
	pet.speech = NULL;
	pet.speech_time = 0;
	pet.bubble_window = 0;
	pet.chasing = False;
	pet.frozen = False;

	set_pet_state(IDLE);
	pick_random_destination();
	create_window();
	grab_keys();
	XSelectInput(dpy, root, KeyPressMask);
}

void show_speech_bubble(const char* s)
{
	if (!s) {
		return;
	}

	if (!font_set) {
		for (const unsigned char* p = (const unsigned char*)s; *p; p++) {
			if (*p & 0x80) {
				/* if no font set, replace it with ":3" */
				s = ":3";
				break;
			}
		}
	}

	pet.speech = s;
	pet.speech_time = 0;
	if (!pet.bubble_window) {
		XSetWindowAttributes attr;
		attr.override_redirect = True;
		attr.background_pixel = WhitePixel(dpy, scr);
		attr.border_pixel = BlackPixel(dpy, scr);

		pet.bubble_window = XCreateWindow(
			dpy, root, 0, 0, 10, 10, 2,
			CopyFromParent, InputOutput, CopyFromParent,
			CWOverrideRedirect | CWBackPixel | CWBorderPixel, &attr
		);
		XSelectInput(dpy, pet.bubble_window, ExposureMask);
	}

	int text_width = 0;
	int text_height = 0;
	if (font_set) {
		XRectangle ink, log;
		XmbTextExtents(font_set, s, (int)strlen(s), &ink, &log);
		text_width = log.width;
		text_height = log.height;
	}
	else {
		GContext gcontext = XGContextFromGC(DefaultGC(dpy, scr));
		XFontStruct* font = XQueryFont(dpy, gcontext);
		if (font) {
			text_width = XTextWidth(font, s, (int)strlen(s));
			text_height = font->ascent + font->descent;
			XFreeFontInfo(NULL, font, 1);
		}
	}

	int bw = CLAMP(text_width + 2 * SPEECH_PAD_X, SPEECH_MIN_W, 1 << 15);
	int bh = CLAMP(text_height + 2 * SPEECH_PAD_Y, SPEECH_MIN_H, 1 << 15);
	int bx = CLAMP(pet.x + 32 - bw / 2, 10, scr_width - 10 - bw);
	int by = CLAMP(pet.y - bh - 10, 10, scr_height - 10 - bh);

	XMoveResizeWindow(dpy, pet.bubble_window, bx, by, bw, bh);
	XMapWindow(dpy, pet.bubble_window);
	draw_bubble();
	XRaiseWindow(dpy, pet.bubble_window);
}

void step(enum state d, double* step_x, double* step_y)
{
	const double s = PET_SPEED;
	const double ds = PET_SPEED * 0.70710678118654752440; /* sqrt(2)^-1 */

	switch (d) {
		case E:  *step_x = s;   *step_y = 0;   break;
		case W:  *step_x = -s;  *step_y = 0;   break;
		case N:  *step_x = 0;   *step_y = -s;  break;
		case S:  *step_x = 0;   *step_y = s;   break;
		case NE: *step_x = ds;  *step_y = -ds; break;
		case SE: *step_x = ds;  *step_y = ds;  break;
		case NW: *step_x = -ds; *step_y = -ds; break;
		case SW: *step_x = -ds; *step_y = ds;  break;
		default: *step_x = 0;   *step_y = 0;   break;
	}
}

void update_animation(void)
{
	if (!pet.current_animation || pet.current_animation->n_frames <= 0) {
		if (pet.state != IDLE) {
			set_pet_state(IDLE);
		}
		return;
	}

	pet.frame_time += PET_REFRESH;
	struct frame* frame_ptr = &pet.current_animation->frames[pet.current_frame];

	if (pet.frame_time >= frame_ptr->duration) {
		pet.frame_time = 0;
		pet.current_frame++;
		if (pet.current_frame >= pet.current_animation->n_frames) {
			pet.current_frame = pet.current_animation->loop ?
				                0 : pet.current_animation->n_frames - 1;
		}
		frame_ptr = &pet.current_animation->frames[pet.current_frame];
		XShapeCombineMask(dpy, pet.window, ShapeBounding, 0, 0, frame_ptr->mask, ShapeSet);
		XSetWindowBackgroundPixmap(dpy, pet.window, frame_ptr->pix);
		XClearWindow(dpy, pet.window);
	}
}

void wander(void)
{
	double delta_x = (double)pet.target_x - pet.subpixel_x;
	double delta_y = (double)pet.target_y - pet.subpixel_y;
	double distance_squared = delta_x * delta_x + delta_y * delta_y;

	if (distance_squared < 4.0) {
		if (pet.wander_wait <= 0) {
			pet.wander_wait =
				WANDER_MIN_WAIT + (rand() % (WANDER_MAX_WAIT - WANDER_MIN_WAIT));
			set_pet_state(IDLE);
		}
		else {
			pet.wander_wait -= PET_REFRESH;
			if (pet.wander_wait <= 0) {
				pick_random_destination();
			}
		}
		return;
	}
	move_to(pet.target_x, pet.target_y);
}

Bool walking(enum state s)
{
	return s == N || s == S || s == E || s == W ||
		   s == NW || s == NE || s == SW || s == SE;
}

void xsleep(long ms)
{
	struct timeval tv = {ms / 1000, (int)((ms % 1000) * 1000)};
	select(0, 0, 0, 0, &tv);
}

int main(int argc, char** argv)
{
	(void)argv;
	if (argc > 1) {
		printf("xpets " VERSION "\n> uint 2025\n");
		return 0;
	}
	setup();
	run();
	return 0;
}
